HashMap
HashMap 主要用来存放键值对,它基于哈希表的Map接口实现,是常用的Java集合之一。
JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜索时间。
底层数据结构分析
HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。
HashMap 有两个影响其性能的参数:初始容量(initial capacity)和负载因子(load factor)。容量就是哈希表中 buckets 的数量,负载因子就是 buckets 填满程度的最大比例。当 buckets 填充的数目大于 capacity * load factor 时,就需要调整 buckets 的数目为当前的 2 倍。 如果对迭代性能要求很高的话不要把 initial capacity 设置过大,也不要把 load factor 设置太小。
默认常量
// 默认的 initial capacity —— 必须是 2 的幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 最大的 capacity
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的加载因子: size 超过 capacity*DEFAULT_LOAD_FACTOR 就会resize
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 使用 tree 而不是 list 的阈值 进化
static final int TREEIFY_THRESHOLD = 8;
// 使用 list 而不是 tree 的阈值 退化
static final int UNTREEIFY_THRESHOLD = 6;
// 当capacity大于64时,才会树化,不然还是链表
static final int MIN_TREEIFY_CAPACITY = 64;
Node结点
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
相关实现
构造函数
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
hash
hashMap 中自己实现了一个 hash 方法,并没有用每个作为key的对象的提供的haseCode()方法,主要的原因是:
当capcity过小的时候,对象hashCode的高位无法参与操作,因此可能产生更多的hash碰撞
因此将高16位与低16位进行异或操作,使全部数位参与。不用 & 或者 | 的原因在于:虽然所有数位都参与了,但可能有部分位没有用,因为 1 & x == 1,0 | x == 0。
tableSizeFor
构造函数中,直接把tableSizeFor(initialCapacity)的值作为阈值。而不是我们认为的 CAP * FACTOR,那是因为初始使用resize时,会重新赋值。
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
tableSizeFor 保证给出的返回值是比给定 capacity 大的2的n次幂次整数。
resize
resize 方法用于初始化或扩容 table 数组的大小。 当 put 时,如果 table 为空,或者 put 完发现当前的 size 已经超过了 threshold ,就会去调用 resize 进行扩容。
resize 大致步骤:
初始化的情况:
当未设置 initialCapacity,newCap 赋值为 DEFAULT_INITIAL_CAPACITY,newThr 赋值为 (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY),按默认的情况也就是 newCap = 16,newThr = 12。。
当设置了 initialCapacity,那么 threshold 也是存在的,就是 tableSizeFor 返回值,是2的n次幂。 newCap 就会设置为这个 threshold,newThr 就会设置为 newCap * loadFactor,所以一开始的设置threshold 只是偷梁换柱,没什么卵用。
分配新buckets,即 Node
[] newTab = (Node [])new Node[newCap]; 然后 return newTab; 结束 这里给出一个初始化的省略方法:
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap==0 && oldThr > 0) // 调用者自己设置了 init cap newCap = oldThr; float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); else { // 调用者自己没设置了 init cap,使用默认的 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; return newTab; }
扩容的情况: 扩容情况没啥好说的,如果超过最大值,则不再扩充,让 threshold = Integer.MAX_VALUE 然后 return oldTab 结束。 没超过最大值,扩容为原来的 2 倍,然后把每个 Node 移动到新的 Node 数组中去。
给出一个省略的方法:
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && //容量翻倍 oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // 阈值翻倍 } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; //如果当前index只有这一个元素,就把他直接移动到新的 tab 里面; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; //如果当前的元素是树结点,对其用树的操作方式 else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); //如果不是树结点,同时还有很多bin,那么进行平移。 //平移比较讲究,不是把每个bin进行rehash,而是判断hash与原数组长度的关系 //如果小于就放到 新数组的 index 处,和原数组对应,同时遍历链表。(因为通常确定index时,如果hash值大于长度,高位是会被抛弃的,现在如果发现如果长度新增加的一位依然没有值,那么他就还在原来的位置) //如果大于就放到 新数组的 index+oldCap 处,同时遍历链表。(长度新增加了一位,那他应该在一个新的位置) //这样我们其实只需要一位的&操作,而不需要全体的重 & 操作,就可以分开两类数据了。 //这样整个时间复杂度就是 O(n) ,无须重新计算每个bin在数组的index else { // preserve order Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
get
其核心方法在于 getNode
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//判断数组不空,长度大于0,且hash位置有值
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//当第一个元素, hash 相等 值相等 就认为是
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//当不止一个元素时,遍历是否相等。
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
put
其核心在于
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//为空就先申请空间
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//当前结点没有元素,直接插入
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//第一个结点就是 key 相等(包括key的hash相等),直接返回第一个结点。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果是树结点,都走树结点的操作。
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//如果是链表结点
else {
for (int binCount = 0; ; ++binCount) {
//整个链表都没有相同的key,直接插入,并判断是否树化。
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//找到了相同的key,就返回这个结点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//更新结点的值。
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//如果 size 超过了 threshold ,重建tab
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
小结:
HashMap 的存储过程
调用 put(K key, V value) 方法进行存储。首先通过 hash(key) 计算出 key 的 hash 值,其中 hash 方法会将 key 的 hashCode 的高 16bit 与低 16bit 进行异或,得到一个 hash 值。然后通过 (n - 1) & hash 得到 bucket 的下标位置。根据 key 和 hash 值寻找是否已存在节点,如果已存在则更新旧值(是否更新旧值根据 onlyIfAbsent 字段决定),不存在的话则调用 newNode 生成新 Node,并存储起来。在 bucket 中寻找的时候通过遍历链表或者红黑树,在 jdk 1.8 中,当 bucket 中碰撞冲突的元素超过某个限制(默认是 8),则使用红黑树替代链表,从而提高查询速度。
capcity长度是2的n次幂
每次扩容都是左移1位,同时使用使用 & 操作来确定数组index,如果不是2的n次幂,部分数位可能出现0,也就是数组元素可能永远都不会有链表在上面。
怎么保证resize的时候,hash分布依然是均匀的
关键在于(e.hash & oldCap)算是均等的分为两份了
线程不安全
多线程的put可能导致元素的丢失:
if ((e = p.next) == null) { // #1 p.next = newNode(hash, key, value, null); // #2 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; }两个线程都断到了#1 的位置,在分别执行#2时,后执行的会把新执行的数据给冲掉。
put和get并发时,可能导致get为null
线程1执行put时,因为元素个数超出threshold而导致resize,线程2此时执行get,有可能导致这个问题。
死循环的问题在 1.8 版本已经解决了。jdk1.8之后有一个tail指向尾指针,不再是头插法,而是尾插法了。
使用自定义类型作为key
一定要重写 hashCode 和 equals 方法。